﻿using ClusteringProblem.Maths.Metrics;
using ClusteringProblem.Model;
using MachineLearning.ClusteringProblem.NumberGenerator;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace ClusteringProblem.Algorithms.AntMethod
{
    public class ModifiedNACRE
    {
         #region Pola

        public delegate void SigmaValueChanged(double sigma);
        public event SigmaValueChanged OnSigmaValueChanged;

        // liczba iteracji
        private int iterationCount;
        // flaga zatrzymania algorytmu
        private bool stopFlag = false;
        // lista wirtualnych agentów - mrówek
        private List<IAntType> ants;
        // generator liczb Mersenne'a
        private MersenneTwister generator;

        // tablice

        private double[,] wArray;
        private double[,] fArray;
        private double[,] vArray;

        // obiekt siatki, kratownicy
        private Plane plane;
        // rozmiar siatki, kratownicy
        private int planeSize;
        // miara odległości
        IDistance distance;
        //macirz odległości
        private double[,] distanceArray;
        //promień r
        private int radius;
        // średnia odległość
        private double dmean;
        // sigma - rozmiar jądra
        private double sigma;
        // początkowa wartość promienia r
        private int initialRadius;
        // maksymalna wartość promienia r
        private int maxRadius;
        // algorytm grupowania
        IClusteringAlgorithm clusteringAlgorithm;
        // wynik grupowania
        private List<Tuple<int, List<IAntType>>> result;
        private bool waitFlag = false;
        

        #endregion

        #region Konstruktor

        /// <summary>
        /// Konstruktor klasy NACRE
        /// </summary>
        /// <param name="plane">siatka</param>
        /// <param name="distance">miara odległości</param>
        /// <param name="clusteringAlgorithm"></param>
        /// <param name="iterationCount">liczba iteracji</param>
        /// <param name="sigma">prametr sigma</param>
        /// <param name="ants">dane</param>
        public ModifiedNACRE(Plane plane, IDistance distance, IClusteringAlgorithm clusteringAlgorithm, int iterationCount, List<IAntType> ants)
        {
            this.generator = new MersenneTwister();
            this.plane = plane;
            this.planeSize = plane.Size;
            this.distance = distance;
            this.clusteringAlgorithm = clusteringAlgorithm;
            this.iterationCount = iterationCount;            
            this.ants = ants;
            this.radius = iterationCount / 10;
            this.maxRadius = ants.Count() / 2;
            this.initialRadius = ants.Count() / 10;
        }

        #endregion

        #region Właściwości

        public bool IsRunning { get; set; }

        public List<Tuple<int, List<IAntType>>> Result
        {
            get 
            { 
                return result; 
            }
            set 
            { 
                result = value; 
            }
        }

        
        public double Sigma
        {
            get { return sigma; }
            set
            {               
                sigma = value;
                if (OnSigmaValueChanged != null)
                {
                    OnSigmaValueChanged(value);
                }                
            }
        }

        #endregion

        #region Metody

        /// <summary>
        /// Uruchamia proces grupowania algorytmem NACRE
        /// </summary>
        public void RunAlgorithm(CancellationTokenSource ct)
        {
            IsRunning = true;
            // losowe rozmieszczenie agentów i wyliczenie macierzy
            InitialPlane();
            ComputeMatrix();            

            // główna pętla algorytmu
            for (int i = 0; i < iterationCount && !Break(); i++)
            {
                // zmniejszenie o 5% sigmy co 10 iteracji
                if (i % 10 == 0)
                {
                    Sigma = sigma * 0.95;
                    Thread.Sleep(50);
                }

                double sumW = 0.0;
                double deltaWX = 0.0;
                double deltaWY = 0.0;
                Dictionary<IAntType, Tuple<double, double>> position = new Dictionary<IAntType, Tuple<double, double>>();
                foreach (IAntType ant in ants)
                {
                    Wait();

                    // pobranie listy sąsiadów agenta
                    List<IAntType> neighboursList = GetNeighboursList(ant);

                    // dla każdej mrówki znajdującej się w sąsiedztwie agenta
                    foreach (IAntType _ant in neighboursList)
                    {
                        if (fArray[ant.Index, _ant.Index] != 0)
                        {
                            if (Break())
                            {
                                break;
                            }

                            Wait();

                            double w = wArray[ant.Index, _ant.Index];

                            sumW += w;
                            deltaWX += w * (_ant.X - ant.X);
                            deltaWY += w * (_ant.Y - ant.Y);
                        }
                    }

                    Wait();

                    if (!Break() && sumW > 0)
                    {
                        position.Add(ant, new Tuple<double, double>(deltaWX / sumW, deltaWY / sumW));
                    }
                }
                
                var newPositionsList = position.Keys.ToList();

                for (int k = 0; k < newPositionsList.Count; ++k)
                {
                    var ant = newPositionsList[k];
                    ant.Move(plane, position[ant].Item1, position[ant].Item2);                    
                }

                Wait();

                if (!Break())
                {
                    UpdateRadius(i);
                }
            }

            //if (CanReportState())
            {
                // uruchomienie algorytmu HAC
                clusteringAlgorithm.RunAlgorithm(ants);
                result = clusteringAlgorithm.Result;

                //ReportPlane();
                //ReportResult();
            }

            IsRunning = false;
            ct.Cancel();

        }

        /// <summary>
        /// Obliczenia macierzy
        /// </summary>
        private void ComputeMatrix()
        {
            distanceArray = new double[planeSize, planeSize];

            // obliczenie odległości między obiektami ich zapis w macierzy
            for (int i = 0; i < planeSize; i++)
            {
                for (int j = 0; j < i; ++j)
                {
                    distanceArray[i, j] = Distance(ants[i], ants[j]);
                    distanceArray[j, i] = distanceArray[i, j];
                }
            }

            dmean = 0;
            for (int i = 0; i < planeSize; i++)
            {
                for (int j = 0; j < planeSize; j++)
                {
                    dmean += (1.0 / (planeSize * planeSize)) * distanceArray[i, j];
                }
            }

            Sigma = dmean / (2 * Math.Sqrt((Math.Log10(2))));
            distanceArray = NormalizeMatrix(distanceArray);                      

            // wyliczenie J
            double _j = ComputeJ(ants[0].Vector.Count(), sigma);
            wArray = new double[planeSize, planeSize];
            fArray = new double[planeSize, planeSize];

            // obliczenie współczynników wagowych i sił informacyjnych Fij
            for (int i = 0; i < planeSize; ++i)
            {
                for (int j = 0; j < i; ++j)
                {
                    wArray[i, j] = ComputeCoefficientW(distanceArray[j, i]);
                    wArray[j, i] = wArray[i, j];

                    var val = _j * wArray[j, i];
                    fArray[i, j] = val;
                    fArray[j, i] = fArray[i, j];
                }
            }

            // obliczenie potencjału informacyjnego - vij
            vArray = new double[planeSize, planeSize];
            for (int i = 0; i < planeSize; ++i)
            {
                for (int j = 0; j < i; ++j)
                {
                    for (int k = 0; k < planeSize; ++k)
                    {
                        if (k != i)
                        {
                            vArray[i, j] += fArray[i, k];
                            vArray[j, i] = vArray[i, j];
                        }
                    }
                }
            }
        }

        private double[,] NormalizeMatrix(double[,] distanceMatrix)
        {
            double min = double.MaxValue, max = double.MinValue;

            for (int i = 0; i < planeSize; ++i)
                for (int j = 0; j < planeSize; ++j)
                {
                    if (distanceMatrix[i, j] > max)
                        max = distanceMatrix[i, j];

                    if (distanceMatrix[i, j] < min)
                        min = distanceMatrix[i, j];
                }

            for (int i = 0; i < planeSize; ++i)
                for (int j = 0; j < planeSize; ++j)
                    distanceMatrix[i, j] = (distanceMatrix[i, j] - min) / (max - min);

            return distanceMatrix;
        }


        /// <summary>
        /// Oblicza współczynnik wagowy - mała omega
        /// </summary>
        /// <param name="d">odległość</param>
        /// <returns></returns>
        private double ComputeCoefficientW(double d)
        {
            return Math.Exp((-Math.Pow(d, 2.0)) / (Math.Pow(2.0 * sigma, 2.0)));
        }

        /// <summary>
        /// Oblicza odległość między obiektem i-tym i j-tym obiektem
        /// </summary>
        /// <param name="i">i-ty obiekt</param>
        /// <param name="j">j-ty obiekt</param>
        /// <returns>odległość</returns>
        private double Distance(IAntType i, IAntType j)
        {
            return distance.ComputeDistance(i, j);
        }

        /// <summary>
        /// Losowa inicjalizacja siatki, kratownicy
        /// </summary>
        private void InitialPlane()
        {
            int x, y;
            // dla każdej mrówki w kolekcji mrówek
            foreach (AntType ant in ants)
            {
                // losowanie miejsca, dopóki siatka nie jest już zajęta
                do
                {
                    x = generator.Next(planeSize - 1);
                    y = generator.Next(planeSize - 1);
                }
                while (!plane.isOccupied(x, y));

                // przypisanie mrówki do wylosowanego miejsca na siatce
                plane[x, y] = ant;
                ant.X = x;
                ant.Y = y;
            }
        }

        /// <summary>
        /// Aktualizacja promienia r
        /// </summary>
        /// <param name="i">i-ta iteracja</param>
        private void UpdateRadius(int i)
        {
            radius = initialRadius + (((maxRadius - initialRadius) * i) / iterationCount);
        }

        /// <summary>
        /// Zwraca listę sąsiadów i-tej mrówki
        /// </summary>
        /// <param name="ant">i-ta mrówka</param>
        /// <returns></returns>
        private List<IAntType> GetNeighboursList(IAntType ant)
        {
            int x = ant.X;
            int y = ant.Y;
            return ants.Where(_x => _x.Index != ant.Index)
                             .Where(_x => Math.Sqrt(Math.Pow(_x.X - x, 2) + Math.Pow(_x.Y - y, 2)) <= radius)
                             .ToList<IAntType>();            
        }

        /// <summary>
        /// Oblicza J(M, sigma)
        /// </summary>
        /// <param name="m">M - liczba wymiarów</param>
        /// <param name="sigma">rozmiar jądra sigma</param>
        /// <returns></returns>
        private double ComputeJ(double m, double sigma)
        {
            return -1.0 / (Math.Pow(2, m + 2) * Math.Pow(Math.PI, m / 2) * Math.Pow(sigma, m + 2));
        }

        public void PauseAlgorithm()
        {
            waitFlag = true;
        }

        public void Stop()
        {
            stopFlag = true;
        }

        private void Wait()
        {
            while (waitFlag)
            {
                if (Break())
                    break;
                Thread.Sleep(5);
            }
        }

        private bool Break()
        {
            return stopFlag;
        }

        #endregion
    }
}
